﻿using System;
using System.Collections.Generic;
using System.Text;

namespace FunctionalCLR.Core {
    public class Lexer {

        protected string source;

        protected int lineNumber;

        protected Token token;

        protected Token lastToken;

        public int LineNumber {
            get {
                return this.lineNumber;
            }
        }

        public Lexer(string source) {
            if (string.IsNullOrEmpty(source)) {
                throw new ArgumentException("source is null");
            }

            this.Index = 0;
            this.source = source;
        }

        /// <summary>
        /// check if there is any tokens remain
        /// </summary>
        /// <returns></returns>
        public bool HasMoreTokens() {
            return this.Index < this.source.Length;
        }

        /// <summary>
        /// current index of lexer
        /// </summary>
        public int Index {
            get;
            set;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public Token PeekToken() {
            int index = this.Index;
            Token t = this.NextToken();
            this.Index = index;

            return t;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="offset"></param>
        /// <returns></returns>
        public Token PeekToken(int offset) {
            Token t = null;
            int index = this.Index;

            for (int i = 0; i < offset; i++) {
                t = this.NextToken();
            }
            this.Index = index;

            return t;
        }

        public string LastComment {
            get;
            set;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public Token NextToken() {
            StringBuilder buffer = new StringBuilder();
            this.lastToken = token;
            token = new Token();

            SkipWhiteSpace();
            char chr = this.source[Index];
            while (chr == '#') {
                string cmt = this.ReadUntil('\n', '\r');
                SkipWhiteSpace();
                chr = this.source[Index];
                if (chr == '#') {
                    this.LastComment = this.LastComment + " " + cmt;
                } else {
                    this.LastComment = cmt;
                }
            }
            if (char.IsLetter(chr)) {
                do {
                    buffer.Append(chr);
                    Index++;
                    if (Index >= this.source.Length) {
                        break;
                    }
                    chr = this.source[Index];
                }
                while (
                    (Char.IsLetterOrDigit(chr) || chr == '.' || chr == '_'));
                token.TokenType = TokenType.Identifier;
            } else if (char.IsNumber(chr)) {
                do {
                    buffer.Append(chr);
                    Index++;
                    if (Index >= this.source.Length) {
                        break;
                    }
                    chr = this.source[Index];
                }
                while (
                    (Char.IsDigit(chr) || chr == '.')
                    && Index < this.source.Length);
                token.TokenType = TokenType.Number;
            } else {
                buffer.Append(chr);
                switch (chr) {
                    case '+':
                        token.TokenType = TokenType.Add;
                        break;
                    case '-':
                        token.TokenType = TokenType.Sub;
                        break;
                    case '*':
                        token.TokenType = TokenType.Mul;
                        break;
                    case '/':
                        token.TokenType = TokenType.Div;
                        break;
                    case '^':
                        token.TokenType = TokenType.Square;
                        break;
                    case '\'':
                        token.TokenType = TokenType.Sqrt;
                        break;
                    case ',':
                        token.TokenType = TokenType.Comma;
                        break;
                    case ';':
                        token.TokenType = TokenType.Semicolon;
                        break;
                    case '(':
                        token.TokenType = TokenType.LeftBrace;
                        break;
                    case ')':
                        token.TokenType = TokenType.RightBrace;
                        break;
                    case ':':
                        char nxx = source[Index + 1];
                        if (nxx == '=') {
                            Index++;
                            token.TokenType = TokenType.Declare;
                        } else {
                            token.TokenType = TokenType.Colon;
                        }
                        break;
                    case '&':
                        token.TokenType = TokenType.And;
                        break;
                    case '|':
                        token.TokenType = TokenType.Or;
                        break;
                    case '!':
                        char nc = source[Index + 1];
                        switch (nc) {
                            case '&':
                                buffer.Append(nc);
                                token.TokenType = TokenType.Nand;
                                Index++;
                                break;
                            case '|':
                                buffer.Append(nc);
                                token.TokenType = TokenType.Nor;
                                Index++;
                                break;
                            case '=':
                                buffer.Append(nc);
                                token.TokenType = TokenType.NotEqual;
                                Index++;
                                break;
                            default:
                                token.TokenType = TokenType.Not;
                                break;
                        }
                        break;
                    case '<':
                        if (source[Index + 1] == '=') {
                            Index++;
                            buffer.Append(source[Index]);
                            token.TokenType = TokenType.LessEqualTo;
                        } else {
                            token.TokenType = TokenType.LessThan;
                        }
                        break;
                    case '>':
                        if (source[Index + 1] == '=') {
                            Index++;
                            buffer.Append(source[Index]);
                            token.TokenType = TokenType.LargerEqualTo;
                        } else {
                            token.TokenType = TokenType.LargerThan;
                        }
                        break;
                    case '[':
                        token.TokenType = TokenType.LeftSquareBrace;
                        break;
                    case ']':
                        token.TokenType = TokenType.RightSquareBrace;
                        break;
                    case '$':
                        token.TokenType = TokenType.Dollar;
                        break;
                    case '{':
                        token.TokenType = TokenType.LeftBracket;
                        break;
                    case '}':
                        token.TokenType = TokenType.RightBracket;
                        break;
                    case '~':
                        token.TokenType = TokenType.Tilde;
                        break;
                    case '@':
                        token.TokenType = TokenType.At;
                        break;
                    case '?':
                        token.TokenType = TokenType.Question;
                        break;
                    case '_':
                        token.TokenType = TokenType.Underline;
                        break;
                    case '=':
                        char nx = source[Index + 1];
                        if (nx == '=') {
                            Index++;
                            buffer.Append(source[Index]);
                            token.TokenType = TokenType.Equal;
                        } else {
                            token.TokenType = TokenType.Assign;
                        }
                        break;
                    case '%':
                        token.TokenType = TokenType.Percent;
                        break;
                    case '\"':
                        token.TokenType = TokenType.String;
                        break;
                    default:
                        throw new UnrecognizedCharacterException(chr);
                }
                Index++;
            }

            token.Value = buffer.ToString();

            return token;
        }

        /// <summary>
        /// 
        /// </summary>
        public void SkipWhiteSpace() {
            char chr = source[Index];
            if (char.IsWhiteSpace(chr)) {
                do {
                    Index++;
                    if (Index >= this.source.Length) {
                        break;
                    }
                    chr = this.source[Index];
                }
                while (char.IsWhiteSpace(chr));
            }
        }

        /// <summary>
        /// Walk through to the end of the line
        /// </summary>
        public string NextLine() {
            string t = ReadUntil('\r', '\n');
            SkipWhiteSpace();

            return t;
        }

        public Token NextToken(params string[] expectedToken) {
            Token actualToken = this.NextToken();
            foreach (string m in expectedToken) {
                if (actualToken.Value == m) {
                    return actualToken;
                }
            }

            throw new TokenNotAsExpectedException(
                expectedToken, actualToken.Value);
        }

        public string ReadUntil(params char[] p) {
            List<char> v = new List<char>(p);
            StringBuilder b = new StringBuilder();

            char c = source[Index];
            while (!v.Contains(c) && Index < this.source.Length) {
                b.Append(c);
                ++Index;
                if (Index >= this.source.Length) {
                    return b.ToString();
                }
                c = source[Index];
                if (c == '\r' || c == '\n') {
                    this.lineNumber++;
                }
            }
            Index++;
            //if (Index < this.source.Length)
            //{
            //    b.Append(c);
            //}

            return b.ToString();
        }

        public char PeekChar() {
            int Index = this.Index;
            char c = (char)0;

            do {
                if (Index < this.source.Length) {
                    c = this.source[Index++];
                } else {
                    break;
                }
            }
            while (char.IsWhiteSpace(c));
            return c;
        }

        public char PreviousChar(int p) {
            return this.source[Index - p];
        }

        public Token LastToken {
            get {
                return lastToken;
            }
        }

        public Token NextToken(params TokenType[] tokenType) {
            Token token = NextToken();
            foreach (TokenType tt in tokenType) {
                if (token.TokenType == tt) {
                    return token;
                }
            }

            throw new TokenNotAsExpectedException(
                tokenType, token.Value);
        }
    }
}
